package com.review.graph;

import com.github.javaparser.StaticJavaParser;
import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration;
import com.github.javaparser.ast.body.MethodDeclaration;
import com.github.javaparser.ast.expr.AnnotationExpr;
import com.github.javaparser.ast.expr.MethodCallExpr;
import com.github.javaparser.resolution.declarations.ResolvedMethodDeclaration;
import com.github.javaparser.symbolsolver.JavaSymbolSolver;
import com.github.javaparser.symbolsolver.resolution.typesolvers.CombinedTypeSolver;
import com.github.javaparser.symbolsolver.resolution.typesolvers.JavaParserTypeSolver;
import com.github.javaparser.symbolsolver.resolution.typesolvers.ReflectionTypeSolver;
import com.review.model.CallGraph;
import com.review.model.CallGraphBuildRequest;

import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.stream.Collectors;

public class CallGraphService {

	public CallGraph build(CallGraphBuildRequest request) throws Exception {
		Path root = Path.of(request.getProjectPath());
		CombinedTypeSolver typeSolver = new CombinedTypeSolver();
		typeSolver.add(new ReflectionTypeSolver());
		typeSolver.add(new JavaParserTypeSolver(root));
		StaticJavaParser.getConfiguration().setSymbolResolver(new JavaSymbolSolver(typeSolver));

		CallGraph graph = new CallGraph();
		List<Path> javaFiles = Files.walk(root)
			.filter(p -> p.toString().endsWith(".java"))
			.collect(Collectors.toList());

		Map<String, Set<String>> interfaceToImpls = new HashMap<>();
		Set<String> configurationClasses = new HashSet<>();
		Map<String, String> beanReturnTypeToImpl = new HashMap<>();

		for (Path file : javaFiles) {
			CompilationUnit cu;
			try { cu = StaticJavaParser.parse(file); } catch (Exception e) { continue; }

			for (ClassOrInterfaceDeclaration cls : cu.findAll(ClassOrInterfaceDeclaration.class)) {
				String classFqn = cls.getFullyQualifiedName().orElse(null);
				if (classFqn == null) continue;
				boolean isConfiguration = hasAnnotation(cls, "org.springframework.context.annotation.Configuration");
				boolean isSpringBean = isConfiguration || hasAnyAnnotation(cls, Set.of(
					"org.springframework.stereotype.Component",
					"org.springframework.stereotype.Service",
					"org.springframework.stereotype.Repository",
					"org.springframework.web.bind.annotation.RestController"
				));
				if (isConfiguration) configurationClasses.add(classFqn);
				if (isSpringBean && cls.isClassOrInterfaceDeclaration()) {
					for (com.github.javaparser.ast.type.ClassOrInterfaceType it : cls.getImplementedTypes()) {
						String ifaceSimple = it.getName().asString();
						String ifaceFqn = null;
						try { ifaceFqn = it.resolve().asReferenceType().getQualifiedName(); } catch (Throwable ignored) {}
						interfaceToImpls.computeIfAbsent(ifaceSimple, k -> new LinkedHashSet<>()).add(classFqn);
						if (ifaceFqn != null) interfaceToImpls.computeIfAbsent(ifaceFqn, k -> new LinkedHashSet<>()).add(classFqn);
					}
				}
				// capture @Bean factory methods
				if (isConfiguration) {
					for (MethodDeclaration md : cls.getMethods()) {
						if (hasAnnotation(md, "org.springframework.context.annotation.Bean")) {
							String retSimple = md.getType().asString();
							beanReturnTypeToImpl.put(retSimple, classFqn);
							try {
								String retFqn = md.getType().resolve().asReferenceType().getQualifiedName();
								beanReturnTypeToImpl.put(retFqn, classFqn);
							} catch (Throwable ignored) {}
						}
					}
				}
			}
		}

		for (Path file : javaFiles) {
			CompilationUnit cu;
			try { cu = StaticJavaParser.parse(file); } catch (Exception e) { continue; }

			cu.findAll(ClassOrInterfaceDeclaration.class).forEach(cls -> {
				String classFqn = cls.getFullyQualifiedName().orElse(null);
				if (classFqn == null) return;
				for (MethodDeclaration md : cls.getMethods()) {
					String methodId = buildMethodId(classFqn, md);
					CallGraph.MethodNode node = new CallGraph.MethodNode();
					node.methodId = methodId;
					node.classFqn = classFqn;
					node.signature = md.getDeclarationAsString(false, false, true);
					node.file = root.relativize(file).toString();
					md.getRange().ifPresent(r -> { node.lineStart = r.begin.line; node.lineEnd = r.end.line; });
					graph.nodes.put(methodId, node);
				}
			});

			cu.findAll(MethodCallExpr.class).forEach(call -> {
				try {
					ResolvedMethodDeclaration r = call.resolve();
					String calleeClass = r.getPackageName().isEmpty() ? r.getClassName() : r.getPackageName() + "." + r.getClassName();
					String calleeId = calleeClass + "#" + r.getName() + "(" + paramTypes(r) + "):" + r.getReturnType().describe();
					Optional<MethodDeclaration> owner = call.findAncestor(MethodDeclaration.class);
					if (owner.isEmpty()) return;
					Optional<ClassOrInterfaceDeclaration> ownerCls = owner.get().findAncestor(ClassOrInterfaceDeclaration.class);
					if (ownerCls.isEmpty()) return;
					String ownerFqn = ownerCls.get().getFullyQualifiedName().orElse(null);
					if (ownerFqn == null) return;
					String callerId = buildMethodId(ownerFqn, owner.get());
					addEdge(graph, callerId, calleeId, "unknown");

					Set<String> impls = interfaceToImpls.getOrDefault(calleeClass, Collections.emptySet());
					if (impls.isEmpty()) {
						String calleeSimple = calleeClass.substring(calleeClass.lastIndexOf('.') + 1);
						impls = interfaceToImpls.getOrDefault(calleeSimple, Collections.emptySet());
					}
					for (String impl : impls) {
						String implMethodId = impl + "#" + r.getName() + "(" + paramTypes(r) + "):" + r.getReturnType().describe();
						addEdge(graph, callerId, implMethodId, "di");
					}
					// Factory @Bean: map return type to configuration class as an implementation
					String beanImpl = beanReturnTypeToImpl.get(calleeClass);
					if (beanImpl == null) {
						String calleeSimple = calleeClass.substring(calleeClass.lastIndexOf('.') + 1);
						beanImpl = beanReturnTypeToImpl.get(calleeSimple);
					}
					if (beanImpl != null) {
						String implMethodId = beanImpl + "#" + r.getName() + "(" + paramTypes(r) + "):" + r.getReturnType().describe();
						addEdge(graph, callerId, implMethodId, "bean");
					}
				} catch (Throwable ignore) {}
			});
		}

		return graph;
	}

	private boolean hasAnyAnnotation(ClassOrInterfaceDeclaration cls, Set<String> annotations) {
		for (AnnotationExpr ann : cls.getAnnotations()) {
			try { String name = ann.resolve().getQualifiedName(); if (annotations.contains(name)) return true; } catch (Throwable ignored) {}
		}
		return false;
	}
	private boolean hasAnnotation(MethodDeclaration md, String annotationFqn) {
		for (AnnotationExpr ann : md.getAnnotations()) {
			try { if (annotationFqn.equals(ann.resolve().getQualifiedName())) return true; } catch (Throwable ignored) {}
		}
		return false;
	}
	private boolean hasAnnotation(ClassOrInterfaceDeclaration cls, String annotationFqn) {
		for (AnnotationExpr ann : cls.getAnnotations()) {
			try { if (annotationFqn.equals(ann.resolve().getQualifiedName())) return true; } catch (Throwable ignored) {}
		}
		return false;
	}

	private void addEdge(CallGraph graph, String caller, String callee, String kind) {
		CallGraph.CallEdge edge = new CallGraph.CallEdge();
		edge.caller = caller;
		edge.callee = callee;
		edge.kind = kind;
		graph.edges.add(edge);
	}

	private String buildMethodId(String classFqn, MethodDeclaration md) {
		String params = md.getParameters().stream().map(p -> p.getType().asString()).collect(Collectors.joining(","));
		String ret = md.getType().asString();
		return classFqn + "#" + md.getNameAsString() + "(" + params + "):" + ret;
	}

	private String paramTypes(ResolvedMethodDeclaration r) {
		List<String> list = new ArrayList<>();
		for (int i = 0; i < r.getNumberOfParams(); i++) list.add(r.getParam(i).getType().describe());
		return String.join(",", list);
	}
}

